<?php
/**
 * Zhub - A PHP Framework For API Developer
 *
 * This file is part of the Zhub package.
 *
 * (c) YiLong Fan <fantiq@163.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @author fanyilong <fantiq@163.com>
 * @copyright 2017 fanyilong. All rights reserved.
 * @package Zhub
 * @date 18-2-27
 */

namespace Zhub\Prototype\Objective\Clazz;


use Zhub\DataStruct\Map;
use Zhub\DataStruct\Struct;
use Zhub\Prototype\Objective\Clazz\Constant\ConstantMap;
use Zhub\Prototype\Objective\Clazz\Constant\ConstantStruct;
use Zhub\Prototype\Objective\Clazz\Exceptions\ClassNotFoundException;
use Zhub\Prototype\Objective\Clazz\Exceptions\InvalidKeyWordException;
use Zhub\Prototype\Objective\Clazz\Method\MethodMap;
use Zhub\Prototype\Objective\Clazz\Method\MethodStruct;
use Zhub\Prototype\Objective\Clazz\Property\PropertyMap;
use Zhub\Prototype\Objective\Clazz\Property\PropertyStruct;
use Zhub\Prototype\Objective\ObjectNameSpace;

class Clazz extends Struct
{
    const NAME_SPACE = 'nameSpace';

    const CLASS_NAME = 'className';

    const FINAL = 'final';

    const ABSTRACT = 'abstract';

    const EXTENDS = 'extends';

    const IMPLEMENTS = 'implements';

    const PROPERTIES = 'properties';

    const CONSTANTS = 'constants';

    const METHODS = 'methods';

    private $clazz = null;

    public $uses = null;

    public function __construct(string $nameSpace, string $className)
    {
        parent::__construct([
            self::NAME_SPACE => $nameSpace,
            self::CLASS_NAME => $className,
        ]);

        $this->uses = new class extends Map{
            public function get(string $key):string
            {
                return $this->getBucket($key);
            }

            public function put(string $key)
            {
                $this->putBucket($key, true);
            }
        };
    }

    public static function initialize(): array
    {
        return [
            self::NAME_SPACE => null,
            self::CLASS_NAME => null,
            self::FINAL => false,
            self::ABSTRACT => false,
            self::EXTENDS => null,
            self::IMPLEMENTS => [],
            self::CONSTANTS => function () {return ConstantMap::class;},
            self::PROPERTIES => function () {return PropertyMap::class;},
            self::METHODS => function () {return MethodMap::class;},
        ];
    }

    public function getNameSpace():string
    {
        return $this->get(self::NAME_SPACE);
    }

    public function setNameSpace(string $nameSpace)
    {
        $this->set(self::NAME_SPACE, $nameSpace);
    }

    public function getClassName():string
    {
        return $this->get(self::CLASS_NAME);
    }

    public function setClassName(string $className)
    {
        $this->set(self::CLASS_NAME, $className);
    }

    public function getFinal():bool
    {
        return $this->get(self::FINAL);
    }

    public function setFinal(bool $final)
    {
        if ($final && $this->getAbstract()) {
            throw new InvalidKeyWordException(
                ClazzEnum::name(ClazzEnum::ABSTRACT),
                ClazzEnum::name(ClazzEnum::FINAL)
            );
        }
        $this->set(self::FINAL, $final);
    }

    public function getAbstract():bool
    {
        return $this->get(self::ABSTRACT);
    }

    public function setAbstract(bool $abstract)
    {
        if ($abstract && $this->getFinal()) {
            throw new InvalidKeyWordException(
                ClazzEnum::name(ClazzEnum::FINAL),
                ClazzEnum::name(ClazzEnum::ABSTRACT)
            );
        }
        $this->set(self::ABSTRACT, $abstract);
    }

    public function getExtends():?string
    {
        return $this->get(self::EXTENDS);
    }

    public function setExtends(?string $extends)
    {
        if (!class_exists($extends)) {
            throw new ClassNotFoundException($extends);
        }
        $this->set(self::EXTENDS, $extends);
    }

    public function getImplements():array
    {
        return $this->get(self::IMPLEMENTS);
    }

    public function setImplements(array $implements)
    {
        $this->set(self::IMPLEMENTS, $implements);
    }

    public function getProperties():PropertyMap
    {
        return $this->get(self::PROPERTIES);
    }

    public function setProperties(PropertyMap $properties)
    {
        $this->set(self::PROPERTIES, $properties);
    }

    public function getConstants():ConstantMap
    {
        return $this->get(self::CONSTANTS);
    }

    public function setConstants(ConstantMap $constants)
    {
        $this->set(self::CONSTANTS, $constants);
    }

    public function getMethods():MethodMap
    {
        return $this->get(self::METHODS);
    }

    public function setMethods(MethodMap $methods)
    {
        $this->set(self::METHODS, $methods);
    }

    public function toString(): string
    {

        $clazzTpl = '<?php' . PHP_EOL . 'namespace ' . $this->getNameSpace() . ';' . PHP_EOL . PHP_EOL .
            '%s' . PHP_EOL;

        $clazzTpl .= 'class ' . $this->getClassName();
        if ($extend = $this->getExtends()) {
            // use
            list($className) = ClazzUtil::parseClassNameSpace($extend);
            $this->uses->put($extend);
            $clazzTpl .= ' extends ' . $className;
        }
        // interface
        $interfaces = $this->getImplements();
        $interfaceCollection = [];
        foreach ($interfaces as $interface) {
            list($className) = ClazzUtil::parseClassNameSpace($interface);
            $interfaceCollection[] = $className;
            $this->uses->put($interface);
        }
        $clazzTpl .= empty($interfaceCollection) ? '' : 'implements ' . implode(', ', array_unique($interfaceCollection));

        $clazzTpl .= PHP_EOL . '{' . PHP_EOL . "%s" . PHP_EOL . '}' . PHP_EOL;


        $template = implode(PHP_EOL, $this->getConstants()->each(function (ConstantStruct $constant) {
            return "\t" . $constant->toString() . PHP_EOL;
        }));
        $template .= PHP_EOL;

        $template .= implode(PHP_EOL, $this->getProperties()->each(function (PropertyStruct $property) {
            return "\t" . $property->toString() . PHP_EOL;
        }));
        $template .= PHP_EOL;


        $template .= implode(PHP_EOL, $this->getMethods()->each(function (MethodStruct $method) {
            $tpl = '';
            foreach (explode(PHP_EOL, $method->toString($this)) as $line) {
                $tpl .= "\t" . $line . PHP_EOL;
            }
            return $tpl;
        }));

        // use
        $useTpl = '';
        foreach ($this->uses->toArray() as $useClazz => $v) {
            $useTpl .= 'use ' . $useClazz . ';' . PHP_EOL;
        }

        return sprintf($clazzTpl, $useTpl, $template);
    }

}